using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Text;
using System.IO;
using System.Xml.Serialization;
using System.Linq;

namespace OneStop.Calculations
{
    #region Enums

    public enum ProfileStatus
    {
        Success = 0,
		CeilingViolation = 1,
		NothingToProcess = 2,
		ProcessingError = 3,
		InfiniteDeco = 4,
        InputFailure = 5
    }

    #endregion

    /// <summary>
    /// TODO -   Force all stops may generate excursions that are not possible. Should be removed or
    ///          more complex ascent process used.
    /// Conducts dive based on inputSegments, knownGases, and an existing model.
    /// Iterates through dive segments updating the Model. When all dive segments are
    /// processed then calls Ascend(0) to return to the surface.
    /// 
    /// The Model can be either null in which case a new model is created, or can be
    /// an existing model with tissue loadings.
    /// 
    /// Gas switching is done on the final ascent if OC deco or bailout is specified. See SetDecoGas()
    /// 
    /// Outputs profile to an ArrayList of dive segments - outputSegments. 
    /// </summary>
    public class Profile : INotifyPropertyChanged
	{
        #region Members

        private ObservableCollection<SegmentDive> inputSegments = new ObservableCollection<SegmentDive>();    // Stores input dive segment objects passed from GUI and enabled
        private ObservableCollection<SegmentAbstract> outputSegments = new ObservableCollection<SegmentAbstract>();   // Stores output segments produced by theis class
        private ObservableCollection<Gas> gases = new ObservableCollection<Gas>();

        // Stores dive gas objects passed from GUI and enabled
        private int currentGasIndex;    // Points to current gas
        private double currentDepth;    // Currend dive depth (msw/fsw)        

        private Gas currentGas;         // Current gas
        private Model model;            // Buhlmann model for this dive
        private double runTime;         // Runtime
        private double ppO2;            // CCR ppO2 or zero for OC
        private bool closedCircuit;     // Flag to store closed circuit / open circuit state
        private bool inFinalAscent;     // Flag for final ascent
        private bool isRepetativeDive;  // For repetative dives
        private bool isOCDeco;          // Whether to use OC deco
        private int surfaceInterval;    // For surface interval
        private string metaData;        // Description of the dive for model persistance
        
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Interface Methods

        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }

        #endregion

        #region Properties

        public bool IsValid
        {
            get
            {
                if (this.Gases.Count < 1 ||
                    this.InputSegments.Count < 1)
                {
                    return false;
                }

                foreach (SegmentDive s in this.InputSegments)
                {
                    if (s.Gas == null) return false;
                }

                return true;
            }
        }


        string _Name;
        public string Name
        {
            get
            {
                return _Name;
            }
            set
            {
                _Name = value;
                NotifyPropertyChanged("Name");
            }
        }

        virtual public object Clone()
        {
            XmlSerializer s = new XmlSerializer(typeof(Profile));

            using (MemoryStream output = new MemoryStream())
            {
                // To write to a file, create a StreamWriter object.
                using (StreamWriter writer = new StreamWriter(output))
                {
                    s.Serialize(writer, this);
                    output.Position = 0;

                    return (Profile)s.Deserialize(output);
                }
            }
        }

        public static Profile Load(Stream input)
        {
            XmlSerializer s = new XmlSerializer(typeof(Profile));
            
            Profile p = (Profile)s.Deserialize(input);

            foreach (SegmentDive d in p.InputSegments)
            {
                d.Gas = p.Gases.FindByShortName(d.Gas);
            }

            return p;
        }

        public void Save(Stream output)
        {
            XmlSerializer s = new XmlSerializer(typeof(Profile));

            StreamWriter writer = new StreamWriter(output);
            s.Serialize(writer, this);
        }

        /// <summary>
        /// 
        /// </summary>
        [XmlIgnore()]
        public ObservableCollection<SegmentAbstract> OutputSegments
        {
            get
            {
                return outputSegments;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public ObservableCollection<SegmentDive> InputSegments
        {
            get
            {
                return inputSegments;
            }

            set
            {
                inputSegments = value;
            }
        }

        /// <summary>
        /// Gets the total runtime for the dive profile. Returns -1 if no dive segments are present.
        /// </summary>
        virtual public double RunTime
        {
            get
            {
                double runtime = -1;

                if (this.OutputSegments.Count > 0)
                {
                    runtime = this.OutputSegments[this.OutputSegments.Count - 1].RunTime;
                }

                return Math.Round(runtime);
            }
        }

        /// <summary>
        /// Gets the total deco for the dive profile. Returns -1 if no dive segments are present.
        /// </summary>
        virtual public double DecoTime
        {
            get
            {
                double decotime = -1;

                if (this.OutputSegments != null)
                {
                    decotime = 0;

                    foreach (SegmentAbstract s in this.OutputSegments)
                    {
                        if (s.Type == SegmentType.DECO)
                        {
                            decotime += s.Time;
                        }
                    }
                }

                return Math.Round(decotime);
            }
        }

        virtual public ObservableCollection<Gas> Gases
        {
            get
            {
                return gases;
            }

            set
            {
                gases = value;
            }
        }

		virtual public Model Model
		{
			get
			{
				return model;
			}

            set
            {
                model = value;
            }			
		}

		virtual public bool IsRepetitiveDive
		{
			get
			{
				return isRepetativeDive;
			}

        }

        virtual public int SurfaceInterval
        {
            get
            {
                return surfaceInterval;
            }

        }

        virtual public bool HasCCRSegment
        {
            get
            {
                foreach (SegmentDive s in this.InputSegments)
                {
                    if (s.Mode == SegmentMode.CCR)
                    {
                        return true;
                    }
                }

                return false;
            }
        }

        virtual public bool IsOCDeco
        {
            get
            {
                return isOCDeco;
            }
            set
            {
                isOCDeco = value;
            }

        }

		/// <summary>
        /// isDiveSegments(): Returns true if there are loaded dive segments, else false means there is nothing to process</summary>
		/// <returns> true if there are dive segments to process
		/// </returns>
		virtual public bool DiveSegments
		{
			get
			{
				if (inputSegments.Count == 0)
					return false;
				return true;
			}
        }



        #endregion

        #region Constructors

        public Profile()
        {
            this.IsOCDeco = Prefs.Current.OcDeco;

            Initialise(null);
        }

        /// <summary>
        /// Constructor for objects of class Profile.
        /// </summary>
        /// <param name="knownSegments"></param>
        /// <param name="knownGases"></param>
        /// <param name="m"></param>
        public Profile(ObservableCollection<SegmentDive> knownSegments, ObservableCollection<Gas> knownGases, Model m)
		{
            Initialise(m);
			
			// Construct list of dive segments from known segments
			foreach (SegmentDive s in knownSegments)
			{
				// Add enabled dive segments only to input segments
				if (s.Enable == true)
					inputSegments.Add(s);
			}

            this.SetGases(knownGases);
        }

        private void Initialise(Model m)
        {
            // Is this a new model or a repetative dive ?
            if (m == null)
            {
                // Initialise new model.
                isRepetativeDive = false;
                model = new Model();
                model.InitialiseModel();
                metaData = "";
            }
            else
            {
                // Model exists
                model = m;

                // TODO - Resources
                metaData = model.MetaData + " *PLUS* ";
                isRepetativeDive = true;

                // Reset the Gradient Factors
                model.InitialiseGradient();
            }
        }

        public void SetGases(ObservableCollection<Gas> knownGases)
        {
            gases = new ObservableCollection<Gas>();

            // construct list of dive gases from known gases
            foreach (Gas g in knownGases)
            {
                g.Volume = 0; // Reset the gas volume to zero

                if (g.Enable == true)
                {
                    Trace.WriteLine("Adding gas " + g);
                    gases.Add(g);
                }
            }
        }

        #endregion

        #region Methods
		
        /// <summary>
        /// Conducts a surface interval by performing a constant depth calculation on air at zero meters.
        /// </summary>
        /// <param name="time"></param>
        /// <returns></returns>
		public virtual ProfileStatus DoSurfaceInterval(int time)
		{
			try
			{
				model.ConstDepth(0, time, 0, 0.79, 0); // Do constant depth of zero on air
			}
			catch (ModelStateException)
			{
				return ProfileStatus.ProcessingError;
			}

			// Note that pAmb is applied in the model so changes in altitude will be handled
			// TODO - this holding and passing back of the SI is not very elegant
			surfaceInterval = time;

			return ProfileStatus.Success;
		}

        public virtual void Reset()
        {            
            this.Model = new Model();
            this.Model.InitialiseModel();
            this.runTime = 0;
        }
		
		/// <summary>
        /// Process the dive.
		/// </summary>
		/// <returns></returns>
		public virtual ProfileStatus DoDive()
		{
            this.OutputSegments.Clear();

            ProfileStatus returnCode;
			SegmentAbstract s1;
			SegmentDive sd;
			double t;
			double deltaDepth;
			bool runtimeFlag = Prefs.Current.RuntimeFlag; // Used to decide if segment represents runtime or segtime
			
			// Check that there are segments to process
			if (!DiveSegments)
                return ProfileStatus.NothingToProcess;
			
			// Set initial state
            s1 = (SegmentAbstract)inputSegments[0]; // Set the first segment and set initial gas from it
            currentGas = s1.Gas;

            gases.Sort(); // Sort gases based on MOD, the Gas natural order
            
			currentDepth = 0;
            ppO2 = s1.Setpoint; // Set initial ppO2 based on first segment
			
            //Determine if we are Open or Closed circuit
			if (ppO2 == 0)
				closedCircuit = false;
			else
				closedCircuit = true;

			inFinalAscent = false; // Flag used to work out when all segments are complete and are in flnal ascent  
			
            foreach (SegmentAbstract s in inputSegments)
			{
				Trace.WriteLine("Processing: " + s);

				if (s.Type == SegmentType.CONST)
				{
					// Should be constant depth segments only
					sd = (SegmentDive) s;
					deltaDepth = sd.Depth - currentDepth; // Has depth changed ?

					// Ascend or descend to dive segment, using existing gas and ppO2 settings
					if (deltaDepth > 0)
					{
						// Segment causes a descent
						try
						{
							model.AscDec(currentDepth, sd.Depth, Prefs.Current.DescentRate, currentGas.FHe, currentGas.FN2, ppO2);
						}
						catch (ModelStateException)
						{
                            return ProfileStatus.ProcessingError;
						}
						// Add segment to output segments
                        outputSegments.Add(new SegmentAscDec(currentDepth, sd.Depth, Prefs.Current.DescentRate, currentGas, ppO2) { Gf = model.Gradient.Gf });
						runTime += deltaDepth / Prefs.Current.DescentRate;
					}
					else if (deltaDepth < 0)
					{
						// Segment causes an ascent. 
						// Call Ascend() to process this as it can require decompression
						Ascend(sd.Depth);
					}
					
					// Now at desired depth so process dive segments.                 
					currentDepth = sd.Depth; // Reset current depth
					ppO2 = sd.Setpoint; // Set ppO2
					currentGas = s.Gas; // Set gas used
					// Process segment. 
					if (sd.Time > 0)
					{
						// Only do this if it is not a waypoint.
						// Interpret first segment time as runtime or segment time depending on runtimeFlag
						if (runtimeFlag)
						{
							runtimeFlag = false; // Do this once only. Make segment == runtime
							try
							{
								model.ConstDepth(sd.Depth, sd.Time - runTime, currentGas.FHe, currentGas.FN2, ppO2);
							}
							catch (ModelStateException)
							{
                                return ProfileStatus.ProcessingError;
							}
							// Add segment to output segments
                            outputSegments.Add(new SegmentDive(sd.Depth, sd.Time - runTime, currentGas, ppO2) { Gf = model.Gradient.Gf });

                            
							runTime = sd.Time; // Reset runTime to segment end time    
							// Update metadata
							// TODO - resources
							metaData = metaData + "Dive to " + sd.Depth + " for " + sd.Time;
						}
						else
						{
							// Segtime is segtime
							try
							{
								model.ConstDepth(sd.Depth, sd.Time, currentGas.FHe, currentGas.FN2, ppO2);
							}
							catch (ModelStateException)
							{
                                return ProfileStatus.ProcessingError;
							}
							// Add segment to output segments
                            outputSegments.Add(new SegmentDive(sd.Depth, sd.Time, currentGas, ppO2) { Gf = model.Gradient.Gf });
							runTime += sd.Time; // update runtime
						}
					}
					else
					{
						// Process waypoint
						// Add waypoint to output segments
                        outputSegments.Add(new SegmentDive(sd.Depth, sd.Time, currentGas, ppO2) { Gf = model.Gradient.Gf });
					}
				}
			}
			
			// Processed all specified segments, now get back to surface
			inFinalAscent = true; // Enables automatic gas selection in Ascend() method

			// Call ascend to move to the surface
			returnCode = Ascend(0);

			// Was there an error ?
            if (returnCode != ProfileStatus.Success)
                return returnCode;
			
            // Calculate runtimes and update the segments
			t = 0;
			            
            foreach (SegmentAbstract s in outputSegments)
			{
				t += s.Time; // Set segment runtime
				s.RunTime = t;
			}

			// Write metadata into the model
			model.MetaData = metaData;

            foreach (SegmentAbstract s in this.OutputSegments)
            {
                int i = this.OutputSegments.IndexOf(s);

                if (s.Type == SegmentType.CONST &&
                    s.Time == 0 &&
                    i > 0)
                {
                    if (s.Gas.ShortName != this.OutputSegments[i - 1].Gas.ShortName)
                    {
                        s.Type = SegmentType.GASSWITCH;
                    }

                    if (s.Setpoint != this.OutputSegments[i - 1].Setpoint)
                    {
                        s.Type = SegmentType.SETPOINTSWITCH;
                    }
                }
            }

            ProcessWarnings(true);
            
            NotifyPropertyChanged("RunTime");
            NotifyPropertyChanged("DecoTime");

            return ProfileStatus.Success;
		}		

        /// <summary>
        /// Ascend to target depth, decompressing if necessary. 
        /// If inFinalAscent then gradient factors start changing, and automatic gas selection is made. 
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
		public virtual ProfileStatus Ascend(double target)
		{
			bool inDecoCycle = false;   // Flag to track if we are in a deco cycle
			bool inAscentCycle = false; // Flag to track if we are in a free ascent cycle as opposed to a deco cycle
			bool forceDecoStop = false; // Flag for forcing every deco stop. // TODO - ALWAYS TRUE IN LOGIC
			double stopTime;            // Used for deco stop time
			double decoStopTime = 0;    // Accumulates deco stop time
			double startDepth;          // Holds depth at start of ascent segment
			double maxMV = 0;           // Holds maximum mvgradient at each stop
			double nextStopDepth;       // Next proposed stop depth
			int control = 0;            // Stores controlling compartment at new depth
		
            //double GetCeiling; // Used to store GetCeiling
			Gas tempGas;
			SegmentDeco decoSegment; // Use for adding deco segments
			
			Trace.WriteLine("\nASCEND: started ascent to: " + target);
			Trace.WriteLine("RT: " + runTime + " ppO2: " + ppO2);

			/*
			* Set up some initial stuff:
			*      Are we surfacing
			*      Are we open circuit deco == bailing out     
			*/
			
			if (inFinalAscent && this.IsOCDeco)
			{
				// Switch to Open circuit deco  
				currentGasIndex = - 1;
				SetDecoGas(currentDepth); // Or pick a better gas. Also sets OC mode
			}

            // Going backwards !
			if (currentDepth < target) return ProfileStatus.ProcessingError;

            // Set initial stop to be the next integral stop depth
            // Are we on a stop depth already ?
            // If not, go to next stop depth
			if ((currentDepth % (int)Prefs.Current.StopDepthIncrement) > 0)
			{
				nextStopDepth = (int) (currentDepth / Prefs.Current.StopDepthIncrement) * (int) Prefs.Current.StopDepthIncrement;
			}
			else
			{
				nextStopDepth = (int) (currentDepth - Prefs.Current.StopDepthIncrement);
			}
			
			// Check in case we are overshooting or hit last stop or any of the other bizzar combinations ...
			if ((nextStopDepth < target) || (currentDepth < Prefs.Current.LastStopDepth))
				nextStopDepth = target;
			else if (currentDepth == Prefs.Current.LastStopDepth)
				nextStopDepth = target;
			else if (nextStopDepth < Prefs.Current.LastStopDepth)
				nextStopDepth = Prefs.Current.LastStopDepth;
			
			startDepth = currentDepth; // Initialise ascent segment start depth
			inAscentCycle = true; // Start in free ascent
			
			// Initialise gradient factor for next (in this case first) stop depth
			model.Gradient.GfAtDepth = nextStopDepth;
			
			// Remember maxM-Value and controlling compartment
			maxMV = model.GetMValue(currentDepth);
			control = model.ControlCompartment();
			
			Trace.WriteLine("Initial stop depth: " + nextStopDepth);
			Trace.WriteLine(" ... GetCeiling is now:" + model.GetCeiling());
			Trace.WriteLine(" ... set m-value gradient for: " + nextStopDepth);
			
			while (currentDepth > target)
			{
                Trace.WriteLine(string.Format("GetCeiling: {0}", model.GetCeiling()));

				// Can we move to the proposed next stop depth ?
				while (forceDecoStop || (nextStopDepth < model.GetCeiling()))
				{
					// Need to decompress .... enter decompression loop
					Trace.WriteLine(" ... entering decompression loop ...");

					inDecoCycle = true;
					forceDecoStop = false; // Only used for first entry into deco stop
                    
					if (inAscentCycle)
					{
						// Finalise last ascent cycle as we are now decompressing
                        if (startDepth > currentDepth)
                        {
                            // Did we ascend at all ?
                            // Add ascent segment 
                            outputSegments.Add(new SegmentAscDec(startDepth, currentDepth, Prefs.Current.AscentRate, currentGas, ppO2) { Gf = model.Gradient.Gf });
                        }

						inAscentCycle = false;
						// TODO - start depth is not re-initialised after first use
					}
					
					// set m-value gradient under the following conditions:
					//      if not in multilevel mode, then set it as soon as we do a decompression cycle
					//      otherwise wait until we are finally surfacing before setting it
					if ((!Prefs.Current.GfMultilevelMode || inFinalAscent) && !model.Gradient.GfSet)
					{
						model.Gradient.GfSlopeAtDepth = currentDepth;
						Trace.WriteLine(" ... m-Value gradient slope set at: " + currentDepth + " GF is:" + model.Gradient.Gf);

						model.Gradient.GfAtDepth = nextStopDepth;
						Trace.WriteLine(" ... set m-value gradient for: " + nextStopDepth + " to:" + model.Gradient.Gf);
					}
					
					// Round up runtime to integral number of minutes - only first time through on this cycle
					if (decoStopTime == 0 && (runTime % Prefs.Current.StopTimeIncrement > 0))
					// Is this not an integral time
					{
						stopTime = (int) (runTime / Prefs.Current.StopTimeIncrement) * Prefs.Current.StopTimeIncrement + Prefs.Current.StopTimeIncrement - runTime;
					}
					else
						stopTime = Prefs.Current.StopTimeIncrement;
					
					// Sanity check the rounding
					if (stopTime == 0)
						stopTime = Prefs.Current.StopTimeIncrement;
					
					Trace.WriteLine(" ... decompressing at depth: " + currentDepth + " for next depth :" + nextStopDepth + " Stop: " + stopTime + " ppO2: " + ppO2);
					
                    // Execute stop
					try
					{
						model.ConstDepth(currentDepth, stopTime, currentGas.FHe, currentGas.FN2, ppO2);
					}
					catch (ModelStateException)
					{
                        return ProfileStatus.ProcessingError;
					}

					decoStopTime += stopTime;
					
                    Trace.WriteLine(" ... GetCeiling1 is now:" + model.GetCeiling());
					
                    // Sanity check decoStopTime for infinite loop
					if (decoStopTime > 5000)
					{
						Trace.WriteLine("Infinite loop on deco stop at " + currentDepth);
                        return ProfileStatus.InfiniteDeco;
					}
				}

				// Finished decompression loop 
				if (inDecoCycle)
				{
					// Finalise last deco cycle ...
					runTime += decoStopTime;

                    // Reset for next depth
					if (Prefs.Current.ForceAllStops) forceDecoStop = true; // ALWAYS TRUE AT THIS POINT
					
					// write deco segment
                    decoSegment = new SegmentDeco(currentDepth, decoStopTime, currentGas, ppO2) { Gf = model.Gradient.Gf };
					decoSegment.MvMax = maxMV;
					decoSegment.GfUsed = model.Gradient.Gf;
					decoSegment.ControlCompartment = control;
					outputSegments.Add(decoSegment);

					Trace.WriteLine(decoSegment);
					
					inDecoCycle = false;
					decoStopTime = 0;
				}
				else if (inAscentCycle)
				{
					// Did not decompress, just ascend
					// TODO - if we enable this code always (remove else if) then model will ascend between deco stops, but ... this causes collateral damage to runtim calculations
					try
					{
						model.AscDec(currentDepth, (double) nextStopDepth, Prefs.Current.AscentRate, currentGas.FHe, currentGas.FN2, ppO2);
					}
					catch (ModelStateException)
					{
                        return ProfileStatus.ProcessingError;
					}

					runTime += (currentDepth - nextStopDepth) / ((- 1.0) * Prefs.Current.AscentRate);
					// TODO - Issue here is that this ascent time is not accounted for in any segments unless it was in an ascent cycle            
				}
				
				// Moved up to next depth ...
				Trace.WriteLine("Now at next stop depth: " + nextStopDepth + " runtime: " + runTime);
				
				currentDepth = nextStopDepth;
				maxMV = model.GetMValue(currentDepth);
				control = model.ControlCompartment();
				
				// Check and switch deco gases
				tempGas = currentGas; // Remember this in case we switch

				if (SetDecoGas(currentDepth) == true)
				{
					// If true we have changed gases
					if (inAscentCycle)
					{
						// To switch gases during ascent need to force a waypoint
						Trace.WriteLine(" ... forcing waypoint for gas switch");
						
                        outputSegments.Add(new SegmentAscDec(startDepth, currentDepth, Prefs.Current.AscentRate, tempGas, ppO2));
						startDepth = currentDepth;
					}
				}
				
				// Set next rounded stop depth
				nextStopDepth = (int)currentDepth - (int)Prefs.Current.StopDepthIncrement;
				
				// Check in case we are overshooting or hit last stop
				if ((nextStopDepth < target) || (currentDepth < Prefs.Current.LastStopDepth))
					nextStopDepth = target;
				else if (currentDepth == Prefs.Current.LastStopDepth)
					nextStopDepth = target;
				else if (nextStopDepth < Prefs.Current.LastStopDepth)
					nextStopDepth = Prefs.Current.LastStopDepth;
				
				if (model.Gradient.GfSet)
				{
					// Update GF for next stop                         
					model.Gradient.GfAtDepth = nextStopDepth;
					
                    Trace.WriteLine(" ... set m-value gradient for: " + nextStopDepth + " to:" + model.Gradient.Gf);
				}
			}
			// Are we still in an ascent segment ?
			if (inAscentCycle)
			{
                outputSegments.Add(new SegmentAscDec(startDepth, currentDepth, Prefs.Current.AscentRate, currentGas, ppO2) { Gf = model.Gradient.Gf });
			}
			
            model.PrintModel();

            return ProfileStatus.Success;
		}
		
		/// <summary>
        /// Estimate gas consumption for all output segments and set this into the respective gas objects.
		/// </summary> 
		public virtual void  DoGasCalcs()
		{
            // Reset the gas volume so that a clean calc is done every time
            foreach (SegmentAbstract s in outputSegments)
            {
                s.Gas.Volume = 0;
            }

            foreach (Gas g in this.Gases)
            {
                g.Volume = 0;
            }
            
			foreach (SegmentAbstract s in outputSegments)
			{
                Gas g = gases.FindByShortName(s.Gas);

                if (g != null)
                {
                    double vol = s.GasUsed();
                    g.Volume += s.GasUsed();
                }
				
                Trace.WriteLine(" ... gas used: " + s.Gas + " - " + (int) s.GasUsed() + " (" + (int) s.Gas.Volume + ")");
			}

		}
		
		/// <summary>
        /// Select appropriate deco gas for the depth specified.
        /// Returns true if a gas switch occured.
		/// </summary>
		/// <param name="depth"></param>
		/// <returns></returns>
		private bool SetDecoGas(double depth)
		{
			Gas g;
			bool finished = false;
			bool gasSwitch = false;
			
			Trace.WriteLine("Evaluating deco gas at " + depth);

			// Check to see if we should be changing gases at all ... if so just return doing nothing
			if (!inFinalAscent)
				return false; // Not ascending yet so no gas switching
			if (!this.IsOCDeco)
				return false; // No OC deco so no bailout
			if (gases.Count == 0)
				return false; // No gases to change to
			
			// If this is the first time that this method is called we need to change to Open Circuit bailout
			if (closedCircuit)
			{
				closedCircuit = false;
				ppO2 = 0;
				currentGas = (Gas) gases[0]; // Select the first gas in the list based on MOD
				currentGasIndex = 0;
			}
			
			// Check and switch deco gases
			while (!finished & (currentGasIndex + 1 < gases.Count))
			{
				// Is there another gas to switch to anyway ?
				g = (Gas) gases[currentGasIndex + 1]; // Look at next gas then
				if (g.MOD >= depth)
				{
					// Check MOD, Can move to this gas ?
					currentGasIndex += 1; // Yes !
					currentGas = g;
					gasSwitch = true;
					
                    Trace.WriteLine(" ... changing gas to " + currentGas);
				}
				else
					finished = true; // Look no more
			}

			return gasSwitch;
        }

        [XmlIgnore()]
        public string WarningsString
        {
            get
            {
                string val = null;

                if (this.Warnings.Count > 0)
                {
                    List<string> wv = new List<string>();

                    foreach (Warning w in this.Warnings)
                    {
                        if (!wv.Contains(w.TypeString))
                        {
                            wv.Add(w.TypeString);
                        }                        
                    }

                    string[] s = new string[wv.Count];

                    for (int i = 0; i < s.Length; i++)
                    {
                        s[i] = wv[i];
                    }

                    val = string.Join("; ", s);
                }

                return val;
            }
        }

        protected List<Warning> ProcessWarnings(bool addtoSegments)
        {
            List<Warning> w = new List<Warning>();

            double previousPPN2 = Prefs.Current.PAmb * 0.79;
            double previousPPO2 = Prefs.Current.PAmb * 0.21;

            foreach (SegmentAbstract s in this.OutputSegments)
            {
                if (Prefs.Current.IsWarningEnabled(WarningType.PPO2High) &&
                    s.Type != SegmentType.DECO &&
                    s.Type != SegmentType.SETPOINTSWITCH &&
                    s.Type != SegmentType.GASSWITCH &&
                    s.Type != SegmentType.ASCENT &&
                    s.PPO2 > Prefs.Current.WarningPPO2Max)
                {
                    Warning wPPO2 = new Warning();

                    wPPO2.Type = WarningType.PPO2High;
                    wPPO2.Description = string.Format("PPO2 High at {0}{1}; PPO2 Value: {2}", s.Depth, Prefs.Current.DepthShortString, s.PPO2.ToString("F2"));

                    w.Add(wPPO2);

                    if (addtoSegments) s.Warnings.Add(wPPO2);
                }

                if (Prefs.Current.IsWarningEnabled(WarningType.PPO2Low) &&
                    s.PPO2 < Prefs.Current.WarningPPO2Min)
                {
                    Warning wPPO2 = new Warning();

                    wPPO2.Type = WarningType.PPO2Low;
                    wPPO2.Description = string.Format("PPO2 Low at {0}{1}; PPO2 Value: {2}", s.Depth, Prefs.Current.DepthShortString, s.PPO2.ToString("F2"));

                    w.Add(wPPO2);

                    if (addtoSegments) s.Warnings.Add(wPPO2);
                }

                if (Prefs.Current.IsWarningEnabled(WarningType.PPN2Spike) &&
                    this.OutputSegments.IndexOf(s) != 0 &&
                    previousPPN2 < s.PPN2)
                {

                    double diff = s.PPN2 - previousPPN2;

                    double percent = diff * 100 / previousPPN2;

                    if (percent / 100 >= Prefs.Current.WarningPPN2SpikePercentageMin)
                    {
                        Warning wPPN2 = new Warning();
                        wPPN2.Type = WarningType.PPN2Spike;
                        wPPN2.Description = string.Format("PPN2 increase at {0}{1}; PPN2 Diff: {2} ({3}%)", s.Depth, Prefs.Current.DepthShortString, diff.ToString("F2"), percent.ToString("F0"));

                        w.Add(wPPN2);

                        if (addtoSegments) s.Warnings.Add(wPPN2);
                    }
                }

                if (Prefs.Current.IsWarningEnabled(WarningType.SubOptimalDecoPPO2) &&
                    s.Type == SegmentType.DECO &&
                    (s.PPO2 < Prefs.Current.WarningOptimalDecoPPO2Min || s.PPO2 > Prefs.Current.WarningOptimalDecoPPO2Max))
                {
                    Warning wDeco = new Warning();

                    wDeco.Type = WarningType.SubOptimalDecoPPO2;
                    wDeco.Description = string.Format("Suboptimal Deco PPO2 at {0}{1}; PPO2 Value: {2}", s.Depth, Prefs.Current.DepthShortString, s.PPO2.ToString("F2"));

                    w.Add(wDeco);

                    if (addtoSegments) s.Warnings.Add(wDeco);
                }

                previousPPO2 = s.PPO2;
                previousPPN2 = s.PPN2;
            }

            return w;
        }

        [XmlIgnore()]
        public List<Warning> Warnings
        {
            get
            {
                return ProcessWarnings(false);
            }
        }

        /// <summary>
        /// Converts the profile to a string, including all segments, O2 and gas calcs.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            bool ocSegment = false;

            sb.AppendFormat("MVPlan.NET GF: {0}-{1}\n", (int)(100 * this.Model.Gradient.GfLow), (int)(100 * this.Model.Gradient.GfHigh)); 

            foreach (SegmentAbstract s in this.OutputSegments)
            {
                sb.AppendLine(s.ToString());

                if (s.Setpoint == 0)
                {
                    ocSegment = true;
                }
            }

            sb.AppendLine();

            sb.AppendFormat("Ascent Rate: {0}{1} per min.\n", -1 * Prefs.Current.AscentRate, Prefs.Current.DepthShortString);
            sb.AppendFormat("Descent Rate: {0}{1} per min.\n", Prefs.Current.DescentRate, Prefs.Current.DepthShortString);

            this.DoGasCalcs();

            if (ocSegment)
            {
                sb.AppendLine();

                sb.AppendFormat("Gas Consumption: Dive {0} l/min; Deco {1} l/min \n", Prefs.Current.RMVDive.ToString("F1"), Prefs.Current.RMVDeco.ToString("F1"));

                foreach (Gas g in this.Gases)
                {
                    if (g.Volume > 0)
                    {
                        sb.AppendLine(g.ToString(true));
                    }
                }
            }

            sb.AppendLine();

            sb.AppendFormat("Gradient Factors: {0}/{1}\n", (int)(this.Model.Gradient.GfLow * 100), (int)(this.Model.Gradient.GfHigh * 100));
            sb.AppendFormat("Otu's: {0}\n", (int)this.Model.OxTox.Otu);
            sb.AppendFormat("CNS: {0}%\n", (int)(this.Model.OxTox.Cns * 100));

            sb.AppendLine();

            sb.AppendLine("Gases:");

            foreach (Gas g in gases)
            {
                sb.AppendLine(g.ToString());
            }

            sb.AppendLine();

            if (this.Warnings.Count > 0)
            {
                sb.AppendLine("Warnings:");

                foreach (Warning w in this.Warnings)
                {
                    sb.AppendLine(w.Description);
                }
            }
            else
            {
                sb.AppendLine("No warnings present.");
            }

            return sb.ToString();
        }

        #endregion
    }
}

